﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MyUtils;

namespace MyHelperDebug.HelpUnitTesting {
	[Obsolete("USE WUNIT!! THIS CLASS ARE NO LONGER SUPPORTED")]
	public abstract class FlowAsserts {
		readonly aspect _aspect;
		readonly List<TestResult> results = new List<TestResult>();

		protected FlowAsserts() {
			_aspect = new aspect(this);
		}

		public event Action AssertCalling = delegate { };
		public event Action AssertCalled = delegate { };

		[TestInitialize]
		public void FlowAssertsInit() {
			DateTimeNow.Stub(DateTime.UtcNow);
		}

		[TestCleanup, DebuggerNonUserCode]
		public void FlowAsserts_CleanUp() {
			Func<TestResult, string> testInfoSelector = x => "[" + (x.Success ? "V":"-") + "] \t" + x.Message; //—―   " V " : "—"
			var all = results.Select(testInfoSelector).Join(Environment.NewLine);
			var fails = results.Where(x => !x.Success).Select(testInfoSelector).Join(Environment.NewLine);
			Console.WriteLine(all);
			if (results.Any(x => !x.Success))
				throw new AssertFailedException(fails);
			if (results.Any(x => x.Message.StartsWith("Inconclusive")))
				throw new AssertInconclusiveException("Inconclusive" + all);
		}

		void result(bool succ, string msg) {
			//if (!succ) {
			//    var sb = new StringBuilder();
			//    foreach (var item in new StackTrace(true).GetFrames()) {
			//        if (item.GetMethod().DeclaringType.Assembly == Assembly.GetExecutingAssembly()) continue;
			//        if (!item.GetFileName().Exists()) continue;
			//        sb.AppendLine("{2} {0} : line {1}".Arg(item.GetFileName(), item.GetFileLineNumber(), item.GetMethod().ToString()));
			//    }
			//}
			results.Add(new TestResult {Success = succ, Message = msg});
		}

		public void True(bool condition) {
			using (_aspect.Run) {
				result(condition, "True({0})".Arg(condition));
			}
		}

		public void False(bool condition) {
			using (_aspect.Run) {
				result(!condition, "False({0})".Arg(condition));
			}
		}

		public bool Equal<T>(T p1, T p2) {
			using (_aspect.Run) {
				bool valid = object.Equals(p1, p2);
				string msg = "\t";
				if (typeof (T) == typeof (double)) {
					double d1 = (p1 as IConvertible).ToDouble(null);
					double d2 = (p2 as IConvertible).ToDouble(null);
					double dif = Math.Abs(d1 - d2);
					valid = dif < .000000001;
					if (valid && dif > 0)
						msg += "Tolerance to {0:0.00E+0}. ".Arg(dif.ConvertTo<double>());
					else if (dif > 0) msg += "Difference {0}. ".Arg(dif);
				} else if (typeof (T) == typeof (DateTime)) {
					var d1 = (DateTime) (object) p1;
					var d2 = (DateTime) (object) p2;
					var dif = new TimeSpan(Math.Abs(d1.Ticks - d2.Ticks));
					valid = dif < TimeSpan.FromSeconds(2);
					if (valid && dif.Exists())
						msg += "Tolerance to {0}. ".Arg(dif.ToSpanString());
					else if (dif.Exists())
						msg += "Difference {0}. ".Arg(dif.ToSpanString());
				} else if (typeof (T) == typeof (TimeSpan)) {
					var d1 = (TimeSpan) (object) p1;
					var d2 = (TimeSpan) (object) p2;
					var dif = new TimeSpan(Math.Abs(d1.Ticks - d2.Ticks));
					valid = dif < TimeSpan.FromSeconds(2);
					if (valid && dif.Exists())
						msg += "Tolerance to {0}. ".Arg(dif.ToSpanString());
					else if (dif.Exists())
						msg += "Difference {0}. ".Arg(dif.ToSpanString());
				}
				result(valid, "Equal<{0}>({1}, {2}) {3}".ArgI(typeof (T).Name, toString(p1, false), toString(p2, false), msg));
				return valid;
			}
		}

		// Workaround of invalid inferring
		public void Equal<T>(IEnumerable<T> expected, IEnumerable<T> actual) {
			Equal(expected, actual as IEnumerable);
		}

		public void Equal(IEnumerable expected, IEnumerable actual) {
			using (_aspect.Run) {
				string logo = "Equal(IEnumerable, IEnumerable)";
				if (expected == null) {
					result(false, logo + ": expected enumerator is null");
					return;
				}
				if (actual == null) {
					result(false, logo + ": actual enumerator is null");
					return;
				}
				IEnumerator expEtor = expected.GetEnumerator();
				IEnumerator actEtor = actual.GetEnumerator();

				//var success = true;
				for (int c = 0;; c++) {
					bool expAnyMore = expEtor.MoveNext();
					bool actAnyMore = actEtor.MoveNext();
					if (!expAnyMore && !actAnyMore) {
						// simultaniously finish
						result(true, logo);
						return;
					} else if (expAnyMore && actAnyMore) {
						// checking element
						object a = actEtor.Current;
						object b = expEtor.Current;
						if (!object.Equals(a, b)) {
							bool useMultiLine = a.ToString().Length > 6;
							result(false, logo + ": Element at index {0} do not match: {3}{4}Expected: {4}`{1}` {3}{4}Actual:       {4}`{2}`".Arg(c, toString(b), toString(a),
								useMultiLine ? "\r\n" : null, useMultiLine ? "\t" : null));
							return;
						}
					} else {
						// different length
						result(false, logo + ": {0} sequence is shorter: {1} elements. {3}: {2}".Arg(
							expAnyMore ? "Actual" : "Expected",
							c,
							toString(expAnyMore ? expEtor.Current : actEtor.Current),
							expAnyMore ? "Missing" : "Surplus"));
						return;
					}
				}
			}
		}

//		public void Equivalent<T>(IEnumerable<T> expected, IEnumerable<T> actual) {
//			Equivalent(expected, actual, "<" + typeof (T).Name + ">");
//		}

//		public void Equivalent(IEnumerable expected, IEnumerable actual) {
//			Equivalent(expected, actual, null);
//		}

		public void Equivalent<T>(IEnumerable<T> expected, IEnumerable<T> actual) {
			using (_aspect.Run) {
				var logo = "Equivalent<{0}>(IEnumerable<>, IEnumerable<>)".Arg(typeof (T).Name);
				if (expected == null) {
					result(false, logo + ": expected enumerator is null");
					return;
				}
				if (actual == null) {
					result(false, logo + ": actual enumerator is null");
					return;
				}

				List<T> actualCache = actual.ToList();

				int actualCacheCount = actualCache.Count;

				bool sorted;
				try {
					actualCache.Sort();
					sorted = true;
				} catch {
					sorted = false;
				}
				T[] expectedCache = expected.ToArray();
				var missings = new List<T>();
				foreach (T item in expectedCache) {
					int i = sorted ? actualCache.BinarySearch(item) : actualCache.IndexOf(item);
					if (i == -1)
						missings.Add(item);
					else
						actualCache.RemoveAt(i);
				}
				List<T> surpluses = actualCache;

				if (surpluses.Count == 0 && missings.Count == 0) result(true, logo);
				else {
					result(false, logo +
						": Expected {0} elements. Actual {1}. Missing {2}. Surplus {3}.".Arg(expectedCache.Length, actualCacheCount, toStringCollectionIfNotEmpty(missings), toStringCollectionIfNotEmpty(surpluses)));
				}
			}
		}

		static string toStringCollectionIfNotEmpty<T>(ICollection<T> col) {
			int maxToDisp = 10;
			return "{0} elements".Arg(col.Count) +
				(col.Count > 0
					? ": [ " + col.Take(maxToDisp).Select(x => toString(x, typeof (T) == typeof (object))).Join(", ")
						+ (col.Count > maxToDisp ? ", .." : null) + " ]"
					: null);
		}

		static string toString(object p) {
			return toString(p, true);
		}

		static string toString(object p, bool describeType) {
			if (p == null) return "null";
			Type type = p.GetType();
			if (type.IsValueType && !type.IsPrimitive && !p.Exists()) return "default" + (describeType ? "({0})".Arg(p.GetType().Name) : null);
			if (p is DateTime) return ((DateTime) p).ToStringFull(); // ("yyyy-MM-dd H:mm ");
			if (p is TimeSpan) return ((TimeSpan) p).ToSpanString();

			string tostr = p.ToString();

			if (tostr == p.GetType().FullName || string.IsNullOrEmpty(tostr.Trim())) {
				IEnumerable<PropertyInfo> pros = p.GetType().GetProperties().Where(x => x.CanRead && x.GetIndexParameters().Length == 0);
				if (pros.Any()) {
					PropertyInfo pro1 = pros.First();
					tostr = pro1.Name + "=" + pro1.GetValue(p, null);
				} else {
					FieldInfo[] fields = p.GetType().GetFields();
					if (fields.Any()) {
						FieldInfo field1 = fields.First();
						tostr = field1.Name + "=" + field1.GetValue(p);
					}
				}
			}

			return describeType
				? "{0}: {1}".Arg(type.Name, p)
				: tostr;
		}

		[Obsolete(null, true), EditorBrowsable(EditorBrowsableState.Never)]
		public override bool Equals(object obj) {
			throw new Exception();
		}

		[Obsolete(null, true), EditorBrowsable(EditorBrowsableState.Never)]
		public new static bool Equals(object objA, object objB) {
			throw new Exception();
		}

		public override int GetHashCode() {
			return base.GetHashCode();
		}

		public void Greater<T>(T p1, T p2) where T : IComparable<T> {
			using (_aspect.Run) {
				bool valid;
				string msg = null;

				if (p1 != null && p2 != null) valid = p1.CompareTo(p2) == 1;
				else {
					msg = "Argument is null";
					valid = false;
				}

				result(valid, "Greater<{3}>({0}, {1})\t{2}".ArgI(toString(p1, false), toString(p2, false), msg, typeof (T).Name));
			}
		}

		public Exception Throws(Action act)
		{
			// TODO remove duplocation with generic method!!
			using (_aspect.Run)
			{
				Exception ex = RecordException(act);
				bool throwed = ex != null;

				results.Add(new TestResult
					{
						Success = throwed,
						Message = "Throws" + (throwed ? " `{0}`".Arg(ex.Message) : null),
					});
				return ex;
			}
		}

		public Exception Throws<T>(Action act) {
			using (_aspect.Run) {
				Exception ex = RecordException(act);
				bool throwed = ex != null;
				bool valid = throwed && ex.GetType() == typeof (T);

				results.Add(new TestResult {
					Success = valid,
					Message = "Throws<{0}>".Arg(typeof (T).Name)
						+ (throwed && !valid ? " actually throw <{0}>".Arg(ex.GetType().Name) : null)
							+ (throwed ? " `{0}`".Arg(ex.Message) : null),
				});
				return ex;
			}
		}

		public Exception NotThrows(Action act) {
			return NotThrows(act, null);
		}

		public Exception NotThrows(Action act, string msg) {
			using (_aspect.Run) {
				Exception ex = RecordException(act);
				result(ex == null, (ex == null ? "Not Throws" : ex.Message) + ". " + msg);
				return ex;
			}
		}

		public static Exception RecordException(Action act) {
			try {
				act();
				return null;
			} catch (Exception ex) {
				return ex;
			}
		}

		public void Match(string pattern, string value) {
			using (_aspect.Run) {
				result(Regex.IsMatch(value, pattern), "IsMatch({0},{1})".Arg(pattern, value));
			}
		}

		public void Inconclusive(string msg) {
			using (_aspect.Run) {
				result(true, "Inconclusive. " + msg);
			}
		}

		public void Inconclusive() {
			using (_aspect.Run) {
				result(true, "Inconclusive");
			}
		}

		public void Wait(Func<bool> cond) {
			Wait(5000, cond);
		}

		public void Wait(int ms, Func<bool> cond) {
			using (_aspect.Run) {
				var logo = "Wait({0}, func)".Arg(TimeSpan.FromMilliseconds(ms).ToSpanString());
				Stopwatch sw = Stopwatch.StartNew();
				bool ok = false;
				while (sw.ElapsedMilliseconds < ms) {
					if (cond()) {
						ok = true;
						break;
					}
				}
				result(ok, logo + "\tElapsed: " + sw.Elapsed.ToSpanString());
			}
		}

		#region Nested type: aspect

		class aspect : IDisposable {
			readonly FlowAsserts _flowAsserts;

			public aspect(FlowAsserts flowAsserts) {
				_flowAsserts = flowAsserts;
			}

			public IDisposable Run {
				get {
					_flowAsserts.AssertCalling();
					return this;
				}
			}

			#region IDisposable Members

			public void Dispose() {
				_flowAsserts.AssertCalled();
			}

			#endregion
		}

		#endregion

		#region Nested type: TestResult

		class TestResult {
			public string Message;
			public bool Success;
		}

		#endregion
	}
}